import type { GlobalVars, SimplifiedNode } from "~/extractors/types.js";

type AnyRecord = Record<string, unknown>;

export interface MinifyOptions {
  keepMetadata?: boolean;
  preserveNodeAnchors?: boolean;
}

const BASE_NODE_FIELDS = [
  "type",
  "children",
  "layout",
  "fills",
  "strokes",
  "effects",
  "text",
  "textStyle",
  "styles",
  "strokeWeights",
  "visible",
  "opacity",
  "borderRadius",
  "strokeWeight",
  "strokeDashes",
  "constraints",
];

const NODE_ANCHOR_FIELDS = ["id", "name", "componentId", "componentProperties"];

const STYLE_REFERENCE_FIELDS = new Set([
  "layout",
  "fills",
  "strokes",
  "effects",
  "textStyle",
  "styles",
  "strokeWeights",
]);

export type ExtractedDesign = {
  metadata?: unknown;
  nodes?: SimplifiedNodeRepresentation[];
  globalVars?: GlobalVars & AnyRecord;
  [key: string]: unknown;
};

export type SimplifiedNodeRepresentation = SimplifiedNode & AnyRecord;

export function createMinifiedDesign(
  data: ExtractedDesign,
  options: MinifyOptions = {},
): { nodes: unknown[]; globalVars: AnyRecord; metadata?: unknown } {
  const { keepMetadata = false, preserveNodeAnchors = false } = options;

  const nodesInput = Array.isArray(data.nodes) ? data.nodes : [];
  const styleCandidates = new Set<string>();
  const allowedNodeFields = buildAllowedNodeFieldSet(preserveNodeAnchors);
  const prunedNodes = pruneNodes(nodesInput, styleCandidates, allowedNodeFields);

  const originalGlobalVars = isRecord(data.globalVars) ? (data.globalVars as AnyRecord) : {};
  const stylesRecord = isRecord(originalGlobalVars.styles)
    ? (originalGlobalVars.styles as Record<string, unknown>)
    : {};

  const nonStyleGlobalVars = { ...originalGlobalVars };
  delete nonStyleGlobalVars.styles;
  collectStrings(nonStyleGlobalVars, styleCandidates);

  if (keepMetadata && data.metadata !== undefined) {
    collectStrings(data.metadata, styleCandidates);
  }

  const styleKeys = new Set(Object.keys(stylesRecord));
  const usedStyles = new Set<string>();

  for (const candidate of styleCandidates) {
    if (styleKeys.has(candidate)) {
      usedStyles.add(candidate);
    }
  }

  const visited = new Set<string>();
  const queue: string[] = Array.from(usedStyles);

  while (queue.length > 0) {
    const current = queue.pop();
    if (!current || visited.has(current)) {
      continue;
    }

    visited.add(current);
    const styleValue = stylesRecord[current];
    if (styleValue === undefined) {
      continue;
    }

    const nestedCandidates = new Set<string>();
    collectStrings(styleValue, nestedCandidates);

    for (const candidate of nestedCandidates) {
      if (styleKeys.has(candidate) && !usedStyles.has(candidate)) {
        usedStyles.add(candidate);
        queue.push(candidate);
      }
    }
  }

  const filteredStyles: Record<string, unknown> = {};
  for (const [styleName, styleValue] of Object.entries(stylesRecord)) {
    if (usedStyles.has(styleName)) {
      const cleanedStyle = cleanValue(styleValue);
      if (cleanedStyle !== undefined) {
        filteredStyles[styleName] = cleanedStyle;
      }
    }
  }

  const cleanedGlobalVars: AnyRecord = { styles: filteredStyles };
  for (const [key, value] of Object.entries(nonStyleGlobalVars)) {
    const cleanedValue = cleanValue(value);
    if (cleanedValue !== undefined) {
      cleanedGlobalVars[key] = cleanedValue;
    }
  }

  const result: { nodes: unknown[]; globalVars: AnyRecord; metadata?: unknown } = {
    nodes: prunedNodes,
    globalVars: cleanedGlobalVars,
  };

  if (keepMetadata && data.metadata !== undefined) {
    const cleanedMetadata = cleanValue(data.metadata);
    if (cleanedMetadata !== undefined) {
      result.metadata = cleanedMetadata;
    }
  }

  return result;
}

function pruneNodes(
  nodes: unknown[],
  styleCandidates: Set<string>,
  allowedNodeFields: Set<string>,
): unknown[] {
  const result: unknown[] = [];
  for (const node of nodes) {
    if (!isRecord(node)) {
      continue;
    }

    const pruned = pruneNode(node, styleCandidates, allowedNodeFields);
    if (pruned) {
      result.push(pruned);
    }
  }
  return result;
}

function pruneNode(
  node: Record<string, unknown>,
  styleCandidates: Set<string>,
  allowedNodeFields: Set<string>,
): AnyRecord | null {
  const pruned: AnyRecord = {};

  for (const key of allowedNodeFields) {
    const rawValue = node[key];
    if (rawValue === undefined) {
      continue;
    }

    if (key === "children") {
      if (Array.isArray(rawValue)) {
        const childNodes = pruneNodes(rawValue, styleCandidates, allowedNodeFields);
        if (childNodes.length > 0) {
          pruned.children = childNodes;
        }
      }
      continue;
    }

    const cleaned = cleanValue(rawValue);
    if (cleaned === undefined) {
      continue;
    }

    if (STYLE_REFERENCE_FIELDS.has(key)) {
      collectStrings(cleaned, styleCandidates);
    }

    pruned[key] = cleaned;
  }

  const nodeType = typeof node.type === "string" ? node.type : undefined;
  pruned.type = typeof pruned.type === "string" ? pruned.type : nodeType ?? "UNSPECIFIED";

  return pruned;
}

function buildAllowedNodeFieldSet(preserveNodeAnchors: boolean): Set<string> {
  const fields = new Set(BASE_NODE_FIELDS);
  if (preserveNodeAnchors) {
    for (const anchor of NODE_ANCHOR_FIELDS) {
      fields.add(anchor);
    }
  }
  return fields;
}

function cleanValue(value: unknown): unknown | undefined {
  if (value === null || value === undefined) {
    return undefined;
  }

  if (typeof value === "string") {
    if (value.length === 0) {
      return undefined;
    }
    return value;
  }

  if (Array.isArray(value)) {
    const cleanedArray = value
      .map((item) => cleanValue(item))
      .filter((item): item is unknown => item !== undefined);
    return cleanedArray.length > 0 ? cleanedArray : undefined;
  }

  if (isRecord(value)) {
    const cleanedEntries: [string, unknown][] = [];
    for (const [key, nestedValue] of Object.entries(value)) {
      const cleanedNested = cleanValue(nestedValue);
      if (cleanedNested !== undefined) {
        cleanedEntries.push([key, cleanedNested]);
      }
    }

    if (cleanedEntries.length === 0) {
      return undefined;
    }

    return Object.fromEntries(cleanedEntries);
  }

  return value;
}

function collectStrings(value: unknown, target: Set<string>): void {
  if (typeof value === "string") {
    target.add(value);
    return;
  }

  if (Array.isArray(value)) {
    for (const item of value) {
      collectStrings(item, target);
    }
    return;
  }

  if (isRecord(value)) {
    for (const nestedValue of Object.values(value)) {
      collectStrings(nestedValue, target);
    }
  }
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}
